#ifndef __INCLUDE_OVIP_THIRD_XS_COUPLEDL2__
#define __INCLUDE_OVIP_THIRD_XS_COUPLEDL2__

#include "xspcomm/xcoroutine.h"
#include "openvip/tilelink.h"
#include <cstring>

namespace ovip {

    class XSCoupledL2Unit
    {
        bool isN    = false;
        int pack_id = 1;
        TLDataC relaseData[2];

        void releaseAddr(u_int64_t addr, uint8_t param, bool write_bk,
                         u_int8_t *ret, bool *cmp, bool *success, int idx,
                         int offset)
        {
            auto opcode = (uint8_t)(write_bk ? TLMessageC::ReleaseData :
                                               TLMessageC::Release);
            auto x      = (TLParam)param;
            switch (x) {
            case TLParam::toT:
                /* code */
                param = (u_int8_t)TLParam::TtoN;
                break;
            case TLParam::toB:
                /* code */
                param = (u_int8_t)TLParam::BtoN;
                break;
            default:
                Assert(false, "Error: %s not supported!",
                       str_TLParam(param, TLParamType::Shrink_Report).c_str());
                break;
            }

            relaseData[0].opcode  = opcode;
            relaseData[0].param   = param;
            relaseData[0].address = addr;
            relaseData[0].size    = this->BEAT_SIZE;
            relaseData[0].source  = this->source;

            relaseData[1].opcode  = opcode;
            relaseData[1].param   = param;
            relaseData[1].address = addr;
            relaseData[1].size    = this->BEAT_SIZE;
            relaseData[1].source  = this->source;

            // Write is interrupted!
            if (this->operation_corrupted && write_bk) {
                Warn("[%ld] %s Write process is interrupted! ", this->clk->clk,
                     this->tl_port->mName.c_str());
                write_bk             = false;
                relaseData[0].opcode = (uint8_t)TLMessageC::Release;
            }

            // Do not need release!
            if (this->operation_corrupted && this->isN) {
                *success = false;
                this->operation_corrupted = false;
                this->busy                = false;
                this->isN                 = false;
                Debug(
                    "[%ld] Cancel release for block: 0x%lx,  (addr:%lx, value: %02x)",
                    this->clk->clk, addr, addr + offset,
                    this->relaseData[idx].data[offset % 32]);
                return;
            }

            this->tl_port->SendReqC2D(
                this->relaseData, write_bk ? 2 : 1,
                [this, ret, cmp, addr, success, idx, offset](
                    TLPort *master, TLData *input, TLData *output, void *args) {
                    *cmp = true;
                    if (ret) { *ret = this->relaseData[idx].data[offset % 32]; }
                    // check success!
                    *success = true; // Get a release response, all operator is success!;
                    this->operation_corrupted = false;
                    this->busy                = false;
                    this->isN                 = false;
                    return false;
                },
                nullptr, "release address: " + xspcomm::sFmt("%lx", addr), 0,
                0);
        }
        uint8_t DATA_BYTES = 64; // 64 Bytes
        uint8_t BEAT_BYTES = 32; // 32 Bytes
        uint8_t BEAT_SIZE  = 6;  // math.ceil(math.log2(DATA_BYTES))
        uint8_t BEAT_NUM   = 2;  // math.ceil(DATA_BYTES / BEAT_BYTES)
        uint8_t MSHR_NUM   = 20;
        uint8_t source;
        xspcomm::XClock *clk;
        bool is_write            = false;
        bool operation_corrupted = false;
        bool busy                = false;
        bool has_BCE             = true;
        u_int64_t current_addr;

    public:
        TLMasterPort *tl_port;
        XSCoupledL2Unit(uint8_t source, std::string name, xspcomm::XPort &dut_port,
                        xspcomm::XClock &clk, std::string prefix, bool has_BCE = true,
                        bool debug = false): has_BCE(has_BCE)
        {
            auto channel_b_handler = [this, debug,
                                      &clk](TLPort *port, TLData *input,
                                            TLData *output, void *args) {
                auto master = (TLMasterPort *)port;
                auto req    = (pTLDataB)input;
                auto resp   = (pTLDataC)output;

                // TLMessageB.ProbeBlock.toN
                resp->param   = req->param;
                resp->address = req->address;
                Assert(req->address % 64 == 0,
                       "Block address need be 64 aliganed!")

                    if (req->opcode == (u_int8_t)TLMessageB::ProbeBlock
                        && req->param == (u_int8_t)TLParam::toN)
                {
                    resp->opcode = (u_int8_t)TLMessageC::ProbeAck;
                    this->operation_corrupted =
                        this->busy && (this->current_addr == req->address);
                    this->isN = true;
                }
                else if (req->opcode == (u_int8_t)TLMessageB::ProbeBlock
                         && req->param == (u_int8_t)TLParam::toB)
                {
                    resp->opcode = (u_int8_t)TLMessageC::ProbeAck;
                    this->operation_corrupted =
                        this->busy && (this->current_addr == req->address);
                    //&& this->is_write;
                }
                else
                {
                    Assert(false, "[%ld] Message from B: %s not supported !",
                           clk.clk, str_TLData(input).c_str());
                }

                if (this->operation_corrupted) {
                    DebugC(true, "[%ld] Probe In busy address: %lx",
                           master->clk->clk, req->address);
                }

                DebugC(
                    debug,
                    "[%ld] HB %s is called: %s => %s [current addr: %lx, %s]",
                    master->clk->clk, master->mName.c_str(),
                    str_TLData(input).c_str(), str_TLData(output).c_str(),
                    this->current_addr, this->busy ? "Busy" : "Free");
                return false;
            };

            auto channel_c_handler = [this, debug](TLPort *port, TLData *input,
                                                   TLData *output, void *args) {
                auto master = (TLMasterPort *)port;
                DebugC(debug, "[%ld] HC %s is called: %s => %s",
                       master->clk->clk, master->mName.c_str(),
                       str_TLData(input).c_str(), str_TLData(output).c_str());
                return false;
            };

            this->source  = source;
            this->clk     = &clk;
            this->tl_port = new TLMasterPort(
                source, name, dut_port, clk, prefix, channel_b_handler,
                channel_c_handler, this->MSHR_NUM, has_BCE, debug);
        };

        bool OK() { return this->busy ? false : true; }

        void CpuLoad(u_int64_t _addr, u_int8_t *ret, bool *cmp,
                        bool *success)
        {
            Assert(this->operation_corrupted == false && this->busy == false,
                   "%s: Load operation is not complete!",
                   this->tl_port->mName.c_str());
            Assert(this->has_BCE, "Need in tilelink Cached mode!");

            this->busy     = true;
            this->is_write = false;

            auto offs = _addr % 64;
            auto addr = _addr - offs;
            auto idx  = (offs >= 32 ? 1 : 0);

            this->current_addr = addr;

            auto opcode = (uint8_t)TLMessageA::AcquireBlock;
            auto param  = (uint8_t)TLParam::NtoB;
            TLDataA req;
            req.opcode  = opcode;
            req.param   = param;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;
            memset(this->relaseData[0].data, 0, TLDataSizeS);
            memset(this->relaseData[1].data, 0, TLDataSizeS);

            this->tl_port->SendReqA2D(
                &req, 1,
                [this, addr, ret, cmp, success, idx,
                 offs](TLPort *master, TLData *rsp_data, TLData *null_data,
                       void *args) {
                    TLDataD resp = *(TLDataD *)rsp_data;
                    Assert(resp.param != (u_int8_t)TLParam::toN,
                           "FIXME: toN cannot read data!");
                    auto index = static_cast<int>((u_int64_t)null_data);
                    memcpy(this->relaseData[index].data, resp.data,
                           TLDataSizeS);
                    if (index == 1)
                        this->releaseAddr(addr, resp.param, true, ret, cmp,
                                          success, idx, offs);
                    Assert(index <= 1, "Index need <= 1");
                    return false;
                },
                nullptr, "read request for address: " + xspcomm::sFmt("%lx", addr), 0);
        }

        void CpuStore(u_int64_t _addr, u_int8_t _data, bool *cmp, bool *success)
        {
            Assert(this->operation_corrupted == false && this->busy == false,
                   "%s: Store operation is not complete!",
                   this->tl_port->mName.c_str());
            Assert(this->has_BCE, "Need in tilelink Cached mode!");

            this->busy     = true;
            this->is_write = true;

            auto offs          = _addr % 64;
            auto addr          = _addr - offs;
            this->current_addr = addr;

            u_int8_t opcode = (u_int8_t)TLMessageA::AcquireBlock;
            u_int8_t param  = (u_int8_t)TLParam::NtoT;
            TLDataA req;
            req.opcode  = opcode;
            req.param   = param;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;
            memset(this->relaseData[0].data, 0, TLDataSizeS);
            memset(this->relaseData[1].data, 0, TLDataSizeS);

            this->tl_port->SendReqA2D(
                &req, 1,
                [this, addr, offs, _data, cmp,
                 success](TLPort *master, TLData *resp_data, TLData *null_data,
                          void *args) {
                    TLDataD resp = *(TLDataD *)resp_data;
                    auto index   = static_cast<int>((u_int64_t)null_data);
                    memcpy(this->relaseData[index].data, resp.data,
                           TLDataSizeS);
                    Assert(resp.param == (u_int8_t)TLParam::toT,
                           "FIXME: only toT can write data! get: %d",
                           resp.param);

                    if (index == 1) {
                        auto idx = (offs >= 32 ? 1 : 0);
                        this->relaseData[idx].data[offs % 32] = _data;
                        this->releaseAddr(addr, resp.param, true, nullptr, cmp,
                                          success, 0, 0);
                    }
                    Assert(index <= 1, "resp data > 2");
                    return false;
                },
                nullptr, "write request.Perm for address: " + xspcomm::sFmt("%lx", addr),
                0);
        }

        void CpuGet(u_int64_t _addr, u_int8_t *ret, bool *cmp,
                        bool *success){
            Assert(!this->has_BCE, "Only TL-U support Get");
            Assert(this->operation_corrupted == false && this->busy == false,
                   "%s: Get operation is not complete!",
                   this->tl_port->mName.c_str());
            
            this->busy     = true;
            this->is_write = false;

            auto offs = _addr % 64;
            auto addr = _addr - offs;
            auto idx  = (offs >= 32 ? 1 : 0);

            this->current_addr = addr;

            auto opcode = (uint8_t)TLMessageA::Get;
            TLDataA req;
            req.opcode  = opcode;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;

            memset(this->relaseData[0].data, 0, TLDataSizeS);
            memset(this->relaseData[1].data, 0, TLDataSizeS);

            this->tl_port->SendReqA2D(
                &req, 1,
                [this, addr, ret, cmp, success, idx,
                 offs](TLPort *master, TLData *rsp_data, TLData *null_data,
                       void *args) {
                    TLDataD resp = *(TLDataD *)rsp_data;
                    Assert(resp.opcode != (u_int8_t)TLMessageD::AccessAckData,
                           "Need AccessAckData");
                    auto index = static_cast<int>((u_int64_t)null_data);
                    memcpy(this->relaseData[index].data, resp.data,
                           TLDataSizeS);
                    if(index == 1){
                        *cmp = true;
                        *success = true;
                        *ret = this->relaseData[idx].data[offs%32];
                        this->busy = false;
                    }
                    return false;
                },
                nullptr, "Get request for address: " + xspcomm::sFmt("%lx", addr), 0);
                        } 
    };

#if ENABLE_XCOROUTINE
     class XSCoupledL2UnitCor
    {
        bool isN    = false;
        bool has_BCE= true;
        TLDataC relaseData[2];
        
        uint8_t DATA_BYTES = 64; // 64 Bytes
        uint8_t BEAT_BYTES = 32; // 32 Bytes
        uint8_t BEAT_SIZE  = 6;  // math.ceil(math.log2(DATA_BYTES))
        uint8_t BEAT_NUM   = 2;  // math.ceil(DATA_BYTES / BEAT_BYTES)
        uint8_t MSHR_NUM   = 20;
        uint8_t source;
        
        bool operation_corrupted = false;
        bool busy                = false;
        u_int64_t current_addr;

        public:
        TLMasterPortCor *tl_port;
        xspcomm::XClock *clk;
        XSCoupledL2UnitCor(uint8_t source, std::string name, xspcomm::XPort &dut_port,
                        xspcomm::XClock &clk, std::string prefix, bool has_BCE = true,
                        bool debug = false){
            this->source  = source;
            this->clk     = &clk;
            this->has_BCE = has_BCE;
            
            auto channel_b_handler = [this, debug,
                                      &clk](TLPort *port, TLData *input,
                                            TLData *output, void *args) {
                auto master = (TLMasterPort *)port;
                auto req    = (pTLDataB)input;
                auto resp   = (pTLDataC)output;

                // TLMessageB.ProbeBlock.toN
                resp->param   = req->param;
                resp->address = req->address;
                DebugC(req->address % 64 != 0, "Find Block address(0x%lx), it need be 64 aliganed!", req->address);

                if (req->opcode == (u_int8_t)TLMessageB::ProbeBlock
                        && req->param == (u_int8_t)TLParam::toN)
                {
                    resp->opcode = (u_int8_t)TLMessageC::ProbeAck;
                    this->operation_corrupted =
                        this->busy && (this->current_addr == req->address);
                    this->isN = true;
                }
                else if (req->opcode == (u_int8_t)TLMessageB::ProbeBlock
                         && req->param == (u_int8_t)TLParam::toB)
                {
                    resp->opcode = (u_int8_t)TLMessageC::ProbeAck;
                    this->operation_corrupted =
                        this->busy && (this->current_addr == req->address);
                }
                else
                {
                    Assert(false, "[%ld] Message from B: %s not supported !",
                           clk.clk, str_TLData(input).c_str());
                }

                if (this->operation_corrupted) {
                    DebugC(true, "[%ld] Probe In busy address: %lx",
                           clk.clk, req->address);
                }

                DebugC(
                    debug,
                    "[%ld] HB %s is called: %s => %s [current addr: %lx, %s]",
                    clk.clk, master->mName.c_str(),
                    str_TLData(input).c_str(), str_TLData(output).c_str(),
                    this->current_addr, this->busy ? "Busy" : "Free");
                return false;
            };

            this->tl_port = new TLMasterPortCor(
                source, name, dut_port, clk, prefix, channel_b_handler, has_BCE, debug);
        }

        xspcomm::xcorutine<> _releaseAddr(u_int64_t addr, u_int8_t param){
            auto opcode = (uint8_t) TLMessageC::ReleaseData;
            switch ((TLParam)param) {
            case TLParam::toT:
                /* code */
                param = (u_int8_t)TLParam::TtoN;
                break;
            case TLParam::toB:
                /* code */
                param = (u_int8_t)TLParam::BtoN;
                break;
            default:
                Assert(false, "Error: %s not supported!",
                       str_TLParam(param, TLParamType::Shrink_Report).c_str());
                break;
            }
            relaseData[0].opcode  = opcode;
            relaseData[0].param   = param;
            relaseData[0].address = addr;
            relaseData[0].size    = this->BEAT_SIZE;
            relaseData[0].source  = this->source;
            relaseData[1].opcode  = opcode;
            relaseData[1].param   = param;
            relaseData[1].address = addr;
            relaseData[1].size    = this->BEAT_SIZE;
            relaseData[1].source  = this->source;
            co_await this->tl_port->SendC2D(std::vector<TLDataC>({relaseData[0], relaseData[1]}));
            co_return;
        }

        xspcomm::xcorutine<u_int8_t> CpuLoad(u_int64_t _addr, bool *success){
            Assert(this->operation_corrupted == false && this->busy == false,
                   "%s: Load operation is not complete!",
                   this->tl_port->mName.c_str());
            Assert(this->has_BCE, "Need in tilelink Cached mode!");
            this->busy     = true;
            auto offs = _addr % 64;
            auto addr = _addr - offs;
            auto idx  = (offs >= 32 ? 1 : 0);
            this->current_addr = addr;

            auto opcode = (uint8_t)TLMessageA::AcquireBlock;
            auto param  = (uint8_t)TLParam::NtoB;
            TLDataA req;
            req.opcode  = opcode;
            req.param   = param;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;
            memset(this->relaseData[0].data, 0, TLDataSizeS);
            memset(this->relaseData[1].data, 0, TLDataSizeS);
            auto ret = co_await this->tl_port->SendA2D(req);
            Assert(ret.size() == 2, "Need 2 response!");
            memcpy(this->relaseData[0].data, ret[0].data, TLDataSizeS);
            memcpy(this->relaseData[1].data, ret[1].data, TLDataSizeS);            
            co_await this->_releaseAddr(addr, ret[1].param);
            *success = this->operation_corrupted == false;

            this->busy = false;
            this->operation_corrupted = false;
            co_return ret[idx].data[offs%32];
        }
        xspcomm::xcorutine<u_int8_t> CpuStore(u_int64_t _addr, u_int8_t _data){
            Assert(this->operation_corrupted == false && this->busy == false,
                   "%s: Store operation is not complete!",
                   this->tl_port->mName.c_str());
            Assert(this->has_BCE, "Need in tilelink Cached mode!");
            this->busy     = true;

            auto offs          = _addr % 64;
            auto addr          = _addr - offs;
            this->current_addr = addr;

            u_int8_t opcode = (u_int8_t)TLMessageA::AcquireBlock;
            u_int8_t param  = (u_int8_t)TLParam::NtoT;
            TLDataA req;
            req.opcode  = opcode;
            req.param   = param;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;
            memset(this->relaseData[0].data, 0, TLDataSizeS);
            memset(this->relaseData[1].data, 0, TLDataSizeS);
            auto ret = co_await this->tl_port->SendA2D(req);

            Assert(ret.size() == 2, "Need 2 response!");
            memcpy(this->relaseData[0].data, ret[0].data, TLDataSizeS);
            memcpy(this->relaseData[1].data, ret[1].data, TLDataSizeS);
            auto idx = (offs >= 32 ? 1 : 0);
            relaseData[idx].data[offs % 32] = _data;
            co_await this->_releaseAddr(addr, ret[0].param);
            bool success = this->operation_corrupted == false;
            this->busy = false;
            this->operation_corrupted = false;
            co_return (u_int8_t)success;
        }

        xspcomm::xcorutine<u_int8_t> CpuGet(u_int64_t _addr, bool *success){
            Assert(!this->has_BCE, "Only TL-U support Get");
            this->busy     = true;
            auto offs = _addr % 64;
            auto addr = _addr - offs;
            auto idx  = (offs >= 32 ? 1 : 0);
            this->current_addr = addr;

            auto opcode = (uint8_t)TLMessageA::Get;
            TLDataA req;
            req.opcode  = opcode;
            req.address = addr;
            req.size    = this->BEAT_SIZE;
            req.source  = this->source;
            auto ret = co_await this->tl_port->SendA2D(req);
            Assert(ret.size() == 2, "Need 2 response!");
            
            *success = this->operation_corrupted == false;
            this->busy = false;
            this->operation_corrupted = false;
            co_return ret[idx].data[offs%32];
        }

    };
#endif
} // namespace ovip::xs

#endif
